import {
  BackSide,
  DoubleSide,
  CubeUVRefractionMapping,
  CubeUVReflectionMapping,
  LinearEncoding,
  ObjectSpaceNormalMap,
  TangentSpaceNormalMap,
  NoToneMapping,
} from '../../constants.js';
import { WebGLProgram } from './WebGLProgram.js';
import { ShaderLib } from '../shaders/ShaderLib.js';
import { UniformsUtils } from '../shaders/UniformsUtils.js';

function WebGLPrograms(renderer, cubemaps, extensions, capabilities, bindingStates, clipping) {
  const programs = [];

  const isWebGL2 = capabilities.isWebGL2;
  const logarithmicDepthBuffer = capabilities.logarithmicDepthBuffer;
  const floatVertexTextures = capabilities.floatVertexTextures;
  const maxVertexUniforms = capabilities.maxVertexUniforms;
  const vertexTextures = capabilities.vertexTextures;

  let precision = capabilities.precision;

  const shaderIDs = {
    MeshDepthMaterial: 'depth',
    MeshDistanceMaterial: 'distanceRGBA',
    MeshNormalMaterial: 'normal',
    MeshBasicMaterial: 'basic',
    MeshLambertMaterial: 'lambert',
    MeshPhongMaterial: 'phong',
    MeshToonMaterial: 'toon',
    MeshStandardMaterial: 'physical',
    MeshPhysicalMaterial: 'physical',
    MeshMatcapMaterial: 'matcap',
    LineBasicMaterial: 'basic',
    LineDashedMaterial: 'dashed',
    PointsMaterial: 'points',
    ShadowMaterial: 'shadow',
    SpriteMaterial: 'sprite',
  };

  const parameterNames = [
    'precision',
    'isWebGL2',
    'supportsVertexTextures',
    'outputEncoding',
    'instancing',
    'instancingColor',
    'map',
    'mapEncoding',
    'matcap',
    'matcapEncoding',
    'envMap',
    'envMapMode',
    'envMapEncoding',
    'envMapCubeUV',
    'lightMap',
    'lightMapEncoding',
    'aoMap',
    'emissiveMap',
    'emissiveMapEncoding',
    'bumpMap',
    'normalMap',
    'objectSpaceNormalMap',
    'tangentSpaceNormalMap',
    'clearcoatMap',
    'clearcoatRoughnessMap',
    'clearcoatNormalMap',
    'displacementMap',
    'specularMap',
    'roughnessMap',
    'metalnessMap',
    'gradientMap',
    'alphaMap',
    'combine',
    'vertexColors',
    'vertexAlphas',
    'vertexTangents',
    'vertexUvs',
    'uvsVertexOnly',
    'fog',
    'useFog',
    'fogExp2',
    'flatShading',
    'sizeAttenuation',
    'logarithmicDepthBuffer',
    'skinning',
    'maxBones',
    'useVertexTexture',
    'morphTargets',
    'morphNormals',
    'premultipliedAlpha',
    'numDirLights',
    'numPointLights',
    'numSpotLights',
    'numHemiLights',
    'numRectAreaLights',
    'numDirLightShadows',
    'numPointLightShadows',
    'numSpotLightShadows',
    'shadowMapEnabled',
    'shadowMapType',
    'toneMapping',
    'physicallyCorrectLights',
    'alphaTest',
    'doubleSided',
    'flipSided',
    'numClippingPlanes',
    'numClipIntersection',
    'depthPacking',
    'dithering',
    'sheen',
    'transmissionMap',
  ];

  function getMaxBones(object) {
    const skeleton = object.skeleton;
    const bones = skeleton.bones;

    if (floatVertexTextures) {
      return 1024;
    } else {
      // default for when object is not specified
      // ( for example when prebuilding shader to be used with multiple objects )
      //
      //  - leave some extra space for other uniforms
      //  - limit here is ANGLE's 254 max uniform vectors
      //    (up to 54 should be safe)

      const nVertexUniforms = maxVertexUniforms;
      const nVertexMatrices = Math.floor((nVertexUniforms - 20) / 4);

      const maxBones = Math.min(nVertexMatrices, bones.length);

      if (maxBones < bones.length) {
        console.warn(`THREE.WebGLRenderer: Skeleton has ${bones.length} bones. This GPU supports ${maxBones}.`);
        return 0;
      }

      return maxBones;
    }
  }

  function getTextureEncodingFromMap(map) {
    let encoding;

    if (map && map.isTexture) {
      encoding = map.encoding;
    } else if (map && map.isWebGLRenderTarget) {
      console.warn(
        "THREE.WebGLPrograms.getTextureEncodingFromMap: don't use render targets as textures. Use their .texture property instead.",
      );
      encoding = map.texture.encoding;
    } else {
      encoding = LinearEncoding;
    }

    return encoding;
  }

  function getParameters(material, lights, shadows, scene, object) {
    const fog = scene.fog;
    const environment = material.isMeshStandardMaterial ? scene.environment : null;

    const envMap = cubemaps.get(material.envMap || environment);

    const shaderID = shaderIDs[material.type];

    // heuristics to create shader parameters according to lights in the scene
    // (not to blow over maxLights budget)

    const maxBones = object.isSkinnedMesh ? getMaxBones(object) : 0;

    if (material.precision !== null) {
      precision = capabilities.getMaxPrecision(material.precision);

      if (precision !== material.precision) {
        console.warn(
          'THREE.WebGLProgram.getParameters:',
          material.precision,
          'not supported, using',
          precision,
          'instead.',
        );
      }
    }

    let vertexShader;
    let fragmentShader;

    if (shaderID) {
      const shader = ShaderLib[shaderID];

      vertexShader = shader.vertexShader;
      fragmentShader = shader.fragmentShader;
    } else {
      vertexShader = material.vertexShader;
      fragmentShader = material.fragmentShader;
    }

    const currentRenderTarget = renderer.getRenderTarget();

    const parameters = {
      isWebGL2,

      shaderID,
      shaderName: material.type,

      vertexShader,
      fragmentShader,
      defines: material.defines,

      isRawShaderMaterial: material.isRawShaderMaterial === true,
      glslVersion: material.glslVersion,

      precision,

      instancing: object.isInstancedMesh === true,
      instancingColor: object.isInstancedMesh === true && object.instanceColor !== null,

      supportsVertexTextures: vertexTextures,
      outputEncoding:
        currentRenderTarget !== null ? getTextureEncodingFromMap(currentRenderTarget.texture) : renderer.outputEncoding,
      map: !!material.map,
      mapEncoding: getTextureEncodingFromMap(material.map),
      matcap: !!material.matcap,
      matcapEncoding: getTextureEncodingFromMap(material.matcap),
      envMap: !!envMap,
      envMapMode: envMap && envMap.mapping,
      envMapEncoding: getTextureEncodingFromMap(envMap),
      envMapCubeUV:
        !!envMap && (envMap.mapping === CubeUVReflectionMapping || envMap.mapping === CubeUVRefractionMapping),
      lightMap: !!material.lightMap,
      lightMapEncoding: getTextureEncodingFromMap(material.lightMap),
      aoMap: !!material.aoMap,
      emissiveMap: !!material.emissiveMap,
      emissiveMapEncoding: getTextureEncodingFromMap(material.emissiveMap),
      bumpMap: !!material.bumpMap,
      normalMap: !!material.normalMap,
      objectSpaceNormalMap: material.normalMapType === ObjectSpaceNormalMap,
      tangentSpaceNormalMap: material.normalMapType === TangentSpaceNormalMap,
      clearcoatMap: !!material.clearcoatMap,
      clearcoatRoughnessMap: !!material.clearcoatRoughnessMap,
      clearcoatNormalMap: !!material.clearcoatNormalMap,
      displacementMap: !!material.displacementMap,
      roughnessMap: !!material.roughnessMap,
      metalnessMap: !!material.metalnessMap,
      specularMap: !!material.specularMap,
      alphaMap: !!material.alphaMap,

      gradientMap: !!material.gradientMap,

      sheen: !!material.sheen,

      transmissionMap: !!material.transmissionMap,

      combine: material.combine,

      vertexTangents: material.normalMap && material.vertexTangents,
      vertexColors: material.vertexColors,
      vertexAlphas:
        material.vertexColors === true &&
        object.geometry &&
        object.geometry.attributes.color &&
        object.geometry.attributes.color.itemSize === 4,
      vertexUvs:
        !!material.map ||
        !!material.bumpMap ||
        !!material.normalMap ||
        !!material.specularMap ||
        !!material.alphaMap ||
        !!material.emissiveMap ||
        !!material.roughnessMap ||
        !!material.metalnessMap ||
        !!material.clearcoatMap ||
        !!material.clearcoatRoughnessMap ||
        !!material.clearcoatNormalMap ||
        !!material.displacementMap ||
        !!material.transmissionMap,
      uvsVertexOnly:
        !(
          !!material.map ||
          !!material.bumpMap ||
          !!material.normalMap ||
          !!material.specularMap ||
          !!material.alphaMap ||
          !!material.emissiveMap ||
          !!material.roughnessMap ||
          !!material.metalnessMap ||
          !!material.clearcoatNormalMap ||
          !!material.transmissionMap
        ) && !!material.displacementMap,

      fog: !!fog,
      useFog: material.fog,
      fogExp2: fog && fog.isFogExp2,

      flatShading: !!material.flatShading,

      sizeAttenuation: material.sizeAttenuation,
      logarithmicDepthBuffer,

      skinning: object.isSkinnedMesh === true && maxBones > 0,
      maxBones,
      useVertexTexture: floatVertexTextures,

      morphTargets: material.morphTargets,
      morphNormals: material.morphNormals,

      numDirLights: lights.directional.length,
      numPointLights: lights.point.length,
      numSpotLights: lights.spot.length,
      numRectAreaLights: lights.rectArea.length,
      numHemiLights: lights.hemi.length,

      numDirLightShadows: lights.directionalShadowMap.length,
      numPointLightShadows: lights.pointShadowMap.length,
      numSpotLightShadows: lights.spotShadowMap.length,

      numClippingPlanes: clipping.numPlanes,
      numClipIntersection: clipping.numIntersection,

      dithering: material.dithering,

      shadowMapEnabled: renderer.shadowMap.enabled && shadows.length > 0,
      shadowMapType: renderer.shadowMap.type,

      toneMapping: material.toneMapped ? renderer.toneMapping : NoToneMapping,
      physicallyCorrectLights: renderer.physicallyCorrectLights,

      premultipliedAlpha: material.premultipliedAlpha,

      alphaTest: material.alphaTest,
      doubleSided: material.side === DoubleSide,
      flipSided: material.side === BackSide,

      depthPacking: material.depthPacking !== undefined ? material.depthPacking : false,

      index0AttributeName: material.index0AttributeName,

      extensionDerivatives: material.extensions && material.extensions.derivatives,
      extensionFragDepth: material.extensions && material.extensions.fragDepth,
      extensionDrawBuffers: material.extensions && material.extensions.drawBuffers,
      extensionShaderTextureLOD: material.extensions && material.extensions.shaderTextureLOD,

      rendererExtensionFragDepth: isWebGL2 || extensions.has('EXT_frag_depth'),
      rendererExtensionDrawBuffers: isWebGL2 || extensions.has('WEBGL_draw_buffers'),
      rendererExtensionShaderTextureLod: isWebGL2 || extensions.has('EXT_shader_texture_lod'),

      customProgramCacheKey: material.customProgramCacheKey(),
    };

    return parameters;
  }

  function getProgramCacheKey(parameters) {
    const array = [];

    if (parameters.shaderID) {
      array.push(parameters.shaderID);
    } else {
      array.push(parameters.fragmentShader);
      array.push(parameters.vertexShader);
    }

    if (parameters.defines !== undefined) {
      for (const name in parameters.defines) {
        array.push(name);
        array.push(parameters.defines[name]);
      }
    }

    if (parameters.isRawShaderMaterial === false) {
      for (let i = 0; i < parameterNames.length; i++) {
        array.push(parameters[parameterNames[i]]);
      }

      array.push(renderer.outputEncoding);
      array.push(renderer.gammaFactor);
    }

    array.push(parameters.customProgramCacheKey);

    return array.join();
  }

  function getUniforms(material) {
    const shaderID = shaderIDs[material.type];
    let uniforms;

    if (shaderID) {
      const shader = ShaderLib[shaderID];
      uniforms = UniformsUtils.clone(shader.uniforms);
    } else {
      uniforms = material.uniforms;
    }

    return uniforms;
  }

  function acquireProgram(parameters, cacheKey) {
    let program;

    // Check if code has been already compiled
    for (let p = 0, pl = programs.length; p < pl; p++) {
      const preexistingProgram = programs[p];

      if (preexistingProgram.cacheKey === cacheKey) {
        program = preexistingProgram;
        ++program.usedTimes;

        break;
      }
    }

    if (program === undefined) {
      program = new WebGLProgram(renderer, cacheKey, parameters, bindingStates);
      programs.push(program);
    }

    return program;
  }

  function releaseProgram(program) {
    if (--program.usedTimes === 0) {
      // Remove from unordered set
      const i = programs.indexOf(program);
      programs[i] = programs[programs.length - 1];
      programs.pop();

      // Free WebGL resources
      program.destroy();
    }
  }

  return {
    getParameters,
    getProgramCacheKey,
    getUniforms,
    acquireProgram,
    releaseProgram,
    // Exposed for resource monitoring & error feedback via renderer.info:
    programs,
  };
}

export { WebGLPrograms };
